CAP 和 PACELC 的适用边界
从一条无法传递的消息推导 CAP
假设三个非拜占庭节点 A/B/C 复制一个寄存器,初值为 0。接口只有 write(value) 和 read(),写完成表示值已被对象接受,读取返回对象的值。我们要求线性化;网络可以分区,A 与 B/C 之间无法传消息,模型也允许这次分区持续不恢复。
| 性质 | 本页采用的含义 | 不能替换成什么 |
|---|---|---|
| C | 原子一致性,即线性化;遵守不重叠操作的真实时间先后 | ACID 中所有业务约束,或“各副本最后大致相同” |
| A | 发给非故障节点的请求最终得到符合对象规范的响应 | 快速返回任意错误、只返回 HTTP 200,或按月统计的成功率 |
| P | 模型允许节点之间发生消息无法及时到达的分区 | 一项可以在网络真的分区时声明关闭的功能 |
按以下顺序操作即可看到冲突。
- 隔离 A 与 B/C。向仍在运行的 A 发出
write(1)。若要求 A,A 不能永远等待另一侧,必须最终完成写入。 - 客户端收到成功之后,才向 B 发出
read()。若要求 A,B 也必须最终回答。 - B 无法区分“另一侧刚完成写 1”和“另一侧根本没有写入”这两个执行。在后一个执行中应返回 0;在前一个执行中,线性化要求返回 1。仅凭 B 可见的信息不能同时做对。
为了保住 C,可以让无法证明最新状态的一侧等待或拒绝;这失去该侧操作的 CAP 可用性。为了保证两侧请求都完成,可以允许某些读写不满足线性化;这要由另一个一致性契约说明。给合法的写操作返回 UNAVAILABLE 是诚实的工程行为,但不能把它计作该寄存器写已按规范完成,以此宣称同时满足 C 与 A。
Gilbert 与 Lynch 的 CAP 证明研究的是明确模型下的不可兼得;它没有说任何系统都只拥有任意两项,也没有把普通超时率、业务不变量或分区恢复算法一并证明。这里的“最终”不等于有限 SLO 截止时间;一个真实服务还须另定多长时间内允许等待。
两笔扣 80 为什么不能靠合并挽救
把对象换成余额 100 的账户,A 是原写主,网络分为 A 与 B/C。A 一侧和 B 一侧各接受一个不同身份的扣 80,分别看到本地余额 100,都向客户端承诺成功,本地都变成 20。
分区恢复后,最后写覆盖可以得到余额 20,但成功承诺总额为 160,与初始 100 无法对账;合并两条扣款得到 -60,又违反非负约束。事后撤销一张成功回执改变了用户已经获得的保证。这里失败的是“两个成功扣款都不可撤销且余额不负”的具体不变量,不能用一个 AP 标签替代说明。
少数侧拒绝扣款可以保护单一裁决历史,但要承认那一侧在分区期间无法完成该操作。另一种契约是两侧耐久接收待确认意图,恢复后只确认一个、拒绝另一个;接纳可用不等于扣款完成可用,用户必须看到待确认状态。若预先把可花额度分配给各侧,且额度总和不超过 100,则可能在额度内本地扣款,但这是改变操作约束、额外建立额度不变量,并非让无限制的同一账户线性化写入绕过 CAP。
一致性模型和 UNKNOWN 历史的定义统一见状态、操作与故障模型。CAP 不能替业务选择“余额是否可负”,也不能决定目录旧值或临时权限错误的损失是否可接受。
PACELC 把正常路径的等待也摆上桌
PACELC 的提问方式是:发生分区时,在可用性与一致性之间如何选择;否则,在延迟与一致性之间如何选择。Abadi 的原文用它补充只讨论分区的分类视角。它是分析框架,不是协议、容量公式或产品永久标签。
例如账户写必须拿到跨区持久确认才返回,而目录写允许先在本区成功、稍后复制。设一个教学路径里,本地持久化需 2 ms,跨区请求往返为 50 ms,远端持久化另需 2 ms。若两步严格串行,本地确认路径约 2 ms,跨区确认路径约 54 ms;若本地和远端持久化可并行,关键路径需要按依赖重新计算。这里是给定输入的路径推算,不是任何产品的性能实测。
异步确认可能降低前台等待,但不能同时声称分区时任意副本都可完成线性化读写。同步复制付出了等待,也仍要回答读从哪里来、何时算提交、故障切换会不会提升落后的副本。批处理和流水线能改善吞吐或摊销成本,不会凭空消除当前操作必须取得的远端证据。
| 候选路径 | 适用承诺 | 失联时的行为与代价 |
|---|---|---|
| 本区缓存读、异步目录复制 | 可接受明确陈旧窗口,冲突可合并或按规则修复 | 可以继续读旧目录;超过业务陈旧期限仍须降级或拒绝,恢复后有反熵与校验责任 |
| 单一权威账户写域、经权威裁决读取 | 扣款承诺不能重复或被恢复覆盖 | 失联侧等待或拒绝,权威侧也须满足提交和读取协议;跨区调用增加延迟 |
| 耐久接收待确认意图 | 接收本身可保证,最终扣款另外查询 | 可提高接纳能力,但队列有积压、超时和失败终态,不能直接展示扣款已完成 |
目录中的安全权限字段未必允许沿用一般展示字段的陈旧容忍度。分类应落到具体操作与损失,不能给整个数据库、整个用户中心一次性贴标签。
quorum 相交只证明集合里有公共成员
固定副本集合大小为 N,成功写落到 W 个副本,读访问 R 个副本。R + W > N 可推出任意这样的读集合与已完成写集合至少有一个公共副本;2W > N 可推出两个写集合相交。公式没有规定公共副本怎样存版本、读取如何裁决、是否回写、哪些值可以返回,也没覆盖临时副本、成员变更或副本丢失持久状态。
下面的寄存器反例采用 N=3、W=2、R=2。读选择见到的最大版本,但不回写,也不判断提交状态。初始各副本为 (v0,0)。
| 步骤 | A | B | C | 客户端观察 |
|---|---|---|---|---|
1. 写 x=1 只到 A,之后调用方失联 | (v1,1) | (v0,0) | (v0,0) | 写未达到 W,结果 UNKNOWN |
| 2. R1 读 A/B,取最大版本 | (v1,1) | (v0,0) | (v0,0) | R1 返回 1 |
| 3. R1 返回后,R2 才读 B/C | (v1,1) | (v0,0) | (v0,0) | R2 返回 0 |
若把那次未完成写补全在 R1 之前,R2 不能倒退为 0;若不补全或把写放在 R2 之后,R1 的 1 就没有合法来源。两次读各访问了 R=2,却没有合法线性化解释。集合相交没有阻止一个读暴露部分写、下一个读又看不到它。仅靠“挑最大时间戳”更无法修复独立墙钟偏移造成的版本误序。
这不表示 quorum 永远不能实现原子寄存器,而是还需要完整协议。可以设计合适的版本生成、读写阶段和读回写;并发多写者又有额外规则。ABD 原子内存论文展示了 quorum 之上还需哪些协议步骤。不能从其证明中只摘一个不等式,再用于完全不同的读写实现。
Raft 多出的规则保护什么
Raft 不是“多数写成功”的别名。它用持久任期与投票限制选举,用日志任期/索引匹配限制复制,用提交规则约束状态机可应用的日志;新领导者不能仅按“哪台活着”或“墙钟最新”来选。详见 Raft 论文第 5 节及共识与关键状态。
考虑任期 1 的 A 把日志 x 写到 A/B 后网络改变。不能只说“两个副本有 x,所以后来任意选主都安全”。若 x 满足该任期的提交规则,其已提交事实必须由后续选举和日志限制保住;若客户端没收到响应,也不能推定 x 未提交。对之前任期的日志,Raft 也不能单凭当前副本数量直接推进提交,而要依赖当前任期日志的提交规则。任意选 C 为新主并覆盖已提交前缀,就破坏了协议所要求的领导者完整性。
多数在线常被当作进展的简写,但它还不充分:多数间要最终能及时通信,节点要持续获得调度,持久状态不能任意丢失,选举超时与消息延迟也要允许稳定领导者形成。在一直抖动、每次选举都被打断的网络里,机器都在线也可能长期没有业务进展。多数暂时离线通常阻止新提交,不等于允许覆盖旧提交;安全性不依赖多数永远在线。
同样,共识排序的是复制状态机命令。命令应用后向外部支付接口再发一次请求,不会因为内部日志只提交一次,就自动只扣一次钱。客户端重试身份、状态机结果去重和外部副作用的协议仍有各自边界。成员变更、稳定存储和读取路径也必须使用实现实际支持的规则,不能把论文名称当部署验收。
实验:同一张故障表上比较两种策略
本实验是有限纸面协议实验,输入为三个节点、初值 0 的寄存器,以及余额 100、两笔各 80 的账户。记录分区前最后确认的版本或日志水位、各侧操作身份、响应内容、待定请求和恢复规则。
- 固定分区为
A | BC,按第一节先在 A 写 1,再在 B 读。分别执行“所有侧都必须完成”和“只有有权裁决的一侧完成”。前者预期无法同时保持线性化;后者要在表中明确标出等待或拒绝的请求,不能把它算成功。 - 两侧各扣 80。对比最后写覆盖、合并流水、只允许一个权威写域、受理待确认意图四种结果,分别核对余额与两张成功回执。预期只有明确限制成功承诺或预先限制本地可用额度的方案,才有机会保住账户不变量。
- 严格重放上一节 quorum 表的三个步骤。预期得到
R1=1 → R2=0的读倒退;指出缺失的是读取协议,而不是把 R 从 2 再口头叫作“多数”。 - 把“多数在线”改成“B/C 在线但双方消息反复超时”。列出选举和提交能否持续推进所需的额外条件,再比较客户端超时与真实提交水位。预期无法仅由存活数量推出完成时间。
这些预期来自所列历史,可以逐步检查;本页没有宣称已搭建或注入故障到三节点 ZooKeeper/Raft 集群。实机验证须使用隔离的三节点环境,控制多数侧与少数侧的双向网络、保存客户端历史和节点提交日志,在解除分区后核对已确认前缀及 UNKNOWN 结果。还要单独覆盖丢回复、旧主恢复、稳定存储重启和成员变更。单节点重启、Redis standalone 或一次无错压测都不具备这个证据范围。
三轮追问:条件变化时重新选择
第一轮:把 W 调到 3,是否可以让读随便走一个本地副本? 在固定成员、写确实在全部三个副本耐久完成之后,没有并发写和恢复回退时,单副本能见到那次完成写;但这不是完整线性化证明。仍需处理部分写被读取、读倒退、并发版本顺序、重启和成员变化,并且 W=3 使任何一个副本失联都无法完成新写。评分看是否把局部交集推论与完整历史分开。
第二轮:三台都在线,为什么 Raft 仍可能不可用? 在线没有保证消息及时到达、调度公平或选举稳定,持续超时会反复分裂投票或更换领导者。先检查任期变化、投票与日志、网络延迟和持久状态,再判断进展前提;不能通过跳过提交规则强行“恢复”。评分看是否给出存活数量之外的条件,并保留安全边界。
第三轮:目录允许 5 秒旧值,分区持续 10 分钟怎么办? 5 秒是业务陈旧上限,不能拿“最终一致”豁免。可在过期后拒绝敏感读、返回明确降级结果,或转向仍能取得权威状态的入口;若继续提供旧展示字段,应另行修改承诺。恢复时还需按版本或合并规则校验,不能只看网络已通。评分看是否把业务期限、降级与修复责任连起来。
闭卷验收
本页 3 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。